package atlantik.ui;

import java.awt.Component;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Point;
import java.util.HashMap;
import java.util.Map;

import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JScrollPane;

import modelz.UpdateListener;
import modelz.ui.PeerMap;
import modelz.ui.PeerMapListener;
import atlantik.game.Atlantik;
import atlantik.game.Estate;
import atlantik.game.Player;
import atlantik.peer.EstatePeer;
import atlantik.peer.TokenPeer;

public class Board extends JPanel {

	private static final long serialVersionUID = 6009335074727417445L;

	private static final float FIELD_ASPECT = 1.5f;

	private final Insets INSETS = new Insets(0, 0, 0, 0);

	private final GridBagConstraints CORNER_FIELD = new GridBagConstraints(0,
			0, 1, 1, FIELD_ASPECT, FIELD_ASPECT, GridBagConstraints.CENTER,
			GridBagConstraints.BOTH, INSETS, 0, 0);

	private final GridBagConstraints HORIZONTAL_FIELD = new GridBagConstraints(
			0, 0, 1, 1, 1, FIELD_ASPECT, GridBagConstraints.CENTER,
			GridBagConstraints.BOTH, INSETS, 0, 0);

	private final GridBagConstraints VERTICAL_FIELD = new GridBagConstraints(0,
			0, 1, 1, FIELD_ASPECT, 1, GridBagConstraints.CENTER,
			GridBagConstraints.BOTH, INSETS, 0, 0);

	private final GridBagConstraints UI_CONSTRAINTS = new GridBagConstraints(0,
			0, 0, 0, GridBagConstraints.REMAINDER,
			GridBagConstraints.REMAINDER, GridBagConstraints.CENTER,
			GridBagConstraints.BOTH, INSETS, 0, 0);

	private LayoutHelper helper;

	private final GridBagLayout layout = new GridBagLayout();

	private final Map<Player, Estate> lastLocations = new HashMap<Player, Estate>();

	protected final Atlantik client;

	PeerMap<Estate, EstatePeer> estatePeers;

	PeerMap<Player, TokenPeer> tokenPeers;

	JScrollPane display = new JScrollPane();

	Board(final Atlantik client) {
		this.client = client;
		estatePeers = new PeerMap<Estate, EstatePeer>(client, client.getState()
				.estates(), EstatePeer.class);
		tokenPeers = new PeerMap<Player, TokenPeer>(client, client.getState()
				.players(), TokenPeer.class);
		setLayout(layout);

		add(display);

		estatePeers.addListener(new PeerMapListener<EstatePeer>() {
			public void itemAdded(EstatePeer peer) {
				add(peer.getComponent());
				update();
			}

			public void itemRemoved(EstatePeer peer) {
				remove(peer.getComponent());
				update();
			}
		});

		tokenPeers.addListener(new PeerMapListener<TokenPeer>() {
			public void itemAdded(final TokenPeer peer) {
				peer.item.addListener(new UpdateListener<Player>() {
					public void updated(Player p) {
						Estate last = lastLocations.get(p);
						Estate next = p.getLocation();

						EstatePeer estate = estatePeers.get(last);
						if (last != null && estate != null)
							estate.remove(peer);

						estate = estatePeers.get(next);
						if (next != null && estate != null) {
							estate.add(peer);
							lastLocations.put(p, next);
						}
					}
				});
			}

			public void itemRemoved(TokenPeer peer) {
				remove(peer.getComponent());
				lastLocations.remove(peer.item);
			}
		});
	}

	public void setDisplay(JComponent c) {
		display.setViewportView(c);
	}

	synchronized void update() {
		int fields = client.getState().estates().size();
		try {
			helper = LayoutHelper.getInstance(fields);
		} catch (IllegalArgumentException e) {
			return;
		}

		GridBagConstraints constraints;
		for (Component component : getComponents()) {
			if (component instanceof EstateView)
				constraints = update((EstateView) component);
			else
				constraints = update(component);

			if (constraints != null)
				layout.setConstraints(component, constraints);
		}
		revalidate();
	}

	private GridBagConstraints update(EstateView p) {
		int id = p.item.getId();

		p.rotation = helper.getBorder(id);

		GridBagConstraints constraints = null;
		if (helper.getIndex(id) == 0)
			constraints = CORNER_FIELD;
		else if ((helper.getBorder(id) % 2) == 0)
			constraints = HORIZONTAL_FIELD;
		else
			constraints = VERTICAL_FIELD;

		return setLocation(constraints, id);
	}

	private GridBagConstraints update(Component c) {
		GridBagConstraints constraints = UI_CONSTRAINTS;
		constraints.gridwidth = helper.borderFieldCount() - 1;
		constraints.gridheight = constraints.gridwidth;
		return setLocation(constraints, new Point(1, 1));
	}

	private GridBagConstraints setLocation(GridBagConstraints constraints,
			int id) {
		return setLocation(constraints, helper.getIndex2D(id));
	}

	private GridBagConstraints setLocation(GridBagConstraints constraints,
			Point p) {
		if (p == null)
			return null;
		constraints.gridx = p.x;
		constraints.gridy = p.y;
		return constraints;
	}

	static class LayoutHelper {
		private static final Map<Integer, LayoutHelper> instances = new HashMap<Integer, LayoutHelper>();

		static LayoutHelper getInstance(int fields)
				throws IllegalArgumentException {
			LayoutHelper instance = instances.get(fields);
			if (instance == null) {
				instance = new LayoutHelper(fields);
				instances.put(fields, instance);
			}
			return instance;
		}

		private final int fields;

		private int borderFieldCount;

		private LayoutHelper(int fields) throws IllegalArgumentException {
			this.fields = fields;
			borderFieldCount = fields / 4;
			if (borderFieldCount == 0 || fields % 4 != 0)
				throw new IllegalArgumentException();
		}

		public int getFields() {
			return fields;
		}

		int borderFieldCount() {
			return borderFieldCount;
		}

		int getBorder(int id) {
			return id / borderFieldCount;
		}

		int getIndex(int id) {
			return id % borderFieldCount;
		}

		Point getIndex2D(int id) {
			switch (getBorder(id)) {
			case 0: // bottom
				return new Point(borderFieldCount - getIndex(id),
						borderFieldCount);
			case 1: // left
				return new Point(0, borderFieldCount - getIndex(id));
			case 2: // top
				return new Point(getIndex(id), 0);
			case 3: // right
				return new Point(borderFieldCount, getIndex(id));
			default:
				return null;
			}
		}
	}
}
